package com.demo.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

/**
 * Spring版本不一样，通知执行顺序可能也会存在差异
 * 下面以Spring4.0版本、Spring5.28版本进行测试
 *
 * Spring4.0
 * 正常情况：环绕前置=====@Before目标方法执行=环绕返回=环绕最终===@After=====@AfterReturning
 * 　　　　异常情况：环绕前置=====@Before目标方法执行=环绕异常=环绕最终===@After=====@AfterThrowing
 *
 * Spring5.28
 * 正常情况：环绕前置=====@Before=目标方法执行=@AfterReturning=====@After=环绕返回=环绕最终
 * 　　　　异常情况：环绕前置=====@Before=目标方法执行=@AfterThrowing=====@After=环绕异常=环绕最终
 *
     这里没有继承接口，aop是如何利用动态代理的？
     1.默认使用 JDK 动态代理，这样便可以代理所有的接口类型（interface）

     2.Spring AOP也支持CGLIB的代理方式。如果我们被代理对象没有实现任何接口或者实现的接口都是空接口，则是CGLIB

     3.我们可以强制使用CGLIB，指定proxy-target-class = “true” 或者 基于注解@EnableAspectJAutoProxy(proxyTargetClass = true)
 ————————————————
 版权声明：本文为CSDN博主「EmineWang」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。
 原文链接：https://blog.csdn.net/a718515028/article/details/119753593
 */
@Aspect
@Component
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class LogAspects {
    private static final Logger log = LoggerFactory.getLogger(LogAspects.class);
    /**
     * 定义切入点，切入点为com.demo.test.aop下的所有函数
     *
     * 定义切入点的时候需要一个包含名字和任意参数的签名，还有一个切入点表达式，如execution(public * com.example.aop…(…))
     *
     * 切入点表达式的格式：execution([可见性]返回类型[声明类型].方法名(参数)[异常])
     * 其中[]内的是可选的，其它的还支持通配符的使用：
     *
     * *：匹配所有字符
     * …：一般用于匹配多个包，多个参数
     * +：表示类及其子类
     * 4)运算符有：&&,||,!
     * 切入点表达式关键词用例：
     * 1）execution：用于匹配子表达式。
     * //匹配com.cjm.model包及其子包中所有类中的所有方法，返回类型任意，方法参数任意
     * @Pointcut(“execution(* com.cjm.model…(…))”)
     * public void before(){}
     *
     * execution(* com.test.aop…*.getuser(…)) 表示指定方法getuser
     */
    @Pointcut("execution(* com.demo.aop..*.*(..))")
    private void webLog(){

    }


    /**
     * 前置通知
     * 在目标方法执行之前执行执行的通知。
     *
     * 前置通知方法，可以没有参数，也可以额外接收一个JoinPoint，Spring会自动将该对象传入，代表当前的连接点，通过该对象可以获取目标对象 和 目标方法相关的信息。
     *
     * 注意，如果接收JoinPoint，必须保证其为方法的第一个参数，否则报错。
     * @param joinPoint
     */
    @Before(value = "webLog()")
    public void methodBefore(JoinPoint joinPoint) {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        //打印请求内容
        log.info("========================【前置通知】请求内容开始======================");
        log.info("请求地址:" + request.getRequestURI());
        log.info("请求方式" + request.getMethod());
        log.info("请求类方法" + joinPoint.getSignature());
        log.info("请求类方法参数" + Arrays.toString(joinPoint.getArgs()));
        log.info("========================【前置通知】请求内容结束======================");

    }

    /**
     * 后置通知
     * 在目标方法执行之后执行的通知。
     * 在后置通知中也可以选择性的接收一个JoinPoint来获取连接点的额外信息，但是这个参数必须处在参数列表的第一个。
     *  后置通知，方法执行报错，执行不下去了，就不会执行了
     * @param o
     */
    @AfterReturning(returning = "o", pointcut = "webLog()")
    //这个注解的作用是:在切入点,return后执行,如果想对某些方法的返回参数进行处理,可以在这操作
    public void methodAfterReturing(Object o) {
        log.info("--------------【AfterReturning - 后置通知】返回内容----------------");
        log.info("Response内容:" + o);
        log.info("--------------【AfterReturning - 后置通知】返回内容----------------");

    }


    /**
     * 是在目标方法执行之后执行的通知,实际是代理方法执行完毕后执行。
     * 最终通知，无论被代理的方法是否被执行，都会执行这个方法，这个方法就是在新生成的代理类执行完后执行
     * 和后置通知不同之处在于，后置通知是在方法正常返回后执行的通知，如果方法没有正常返-例如抛出异常，则后置通知不会执行。
     * 而最终通知无论如何都会在目标方法调用过后执行，即使目标方法没有正常的执行完成。
     *
     * 另外，后置通知可以通过配置得到返回值，而最终通知无法得到。
     *
     * 最终通知也可以额外接收一个JoinPoint参数，来获取目标对象和目标方法相关信息，但一定要保证必须是第一个参数。
     * @param joinpoint
     */
    @After(value = "webLog()")
    public void logEnd(JoinPoint joinpoint) {

        log.info("--------------【After - 最终通知】----------------");
    }

    /**
     * 在目标方法抛出异常时执行的通知
     *
     * 可以配置传入JoinPoint获取目标对象和目标方法相关信息，但必须处在参数列表第一位
     *
     * 另外，还可以配置参数，让异常通知可以接收到目标方法抛出的异常对象。
     * @param object
     */

    @AfterThrowing(value = "webLog()",throwing = "object")
    public void logEnd(Exception  object) {

        log.info("--------------【异常通知】---------------==》" + object);
    }

    /**
     * 2.环绕通知
     * 在目标方法执行之前和之后都可以执行额外代码的通知。
     *
     * 在环绕通知中必须显式的调用目标方法，目标方法才会执行，这个显式调用时通过ProceedingJoinPoint来实现的，可以在环绕通知中接收一个此类型的形参，spring容器会自动将该对象传入，注意这个参数必须处在环绕通知的第一个形参位置。
     *
     * **要注意，只有环绕通知可以接收ProceedingJoinPoint，而其他通知只能接收JoinPoint。
     *
     * 环绕通知需要返回返回值，否则真正调用者将拿不到返回值，只能得到一个null。
     *
     * 环绕通知有控制目标方法是否执行、有控制是否返回值、有改变返回值的能力。
     *
     * 环绕通知虽然有这样的能力，但一定要慎用，不是技术上不可行，而是要小心不要破坏了软件分层的“高内聚 低耦合”的目标。
     * @param pjp
     * @return
     */

    @Around("webLog()")
    public Object arroundFunc(ProceedingJoinPoint pjp) {
        try {
            Object result = null;
            Object[] args = pjp.getArgs();
            beforeFunc();//前置通知
            result = pjp.proceed(args);
            afterReturnFunc();//后置通知
            return result;
        } catch (Throwable t) {
            afterThrowFunc();//异常通知
            throw new RuntimeException(t);
        } finally {
            afterFunc();//最终通知
        }
    }

    void beforeFunc(){
        log.info("--------------【环绕通知-前置通知】---------------==》");
    }

    void afterReturnFunc(){
        log.info("--------------【环绕通知-后置通知】---------------==》");
    }

    public void afterThrowFunc() {
        log.info("--------------【环绕通知-异常通知】---------------==》");
    }

    public void afterFunc() {
        log.info("--------------【环绕通知-最终通知】---------------==》");
    }
}

